1   package org.apache.solr.cloud;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one or more
5    * contributor license agreements.  See the NOTICE file distributed with
6    * this work for additional information regarding copyright ownership.
7    * The ASF licenses this file to You under the Apache License, Version 2.0
8    * (the "License"); you may not use this file except in compliance with
9    * the License.  You may obtain a copy of the License at
10   *
11   *     http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   */
19  
20  import java.io.File;
21  import java.io.IOException;
22  import java.lang.invoke.MethodHandles;
23  import java.net.URL;
24  import java.util.ArrayList;
25  import java.util.Collection;
26  import java.util.Collections;
27  import java.util.HashMap;
28  import java.util.HashSet;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.concurrent.atomic.AtomicInteger;
32  
33  import com.carrotsearch.randomizedtesting.rules.SystemPropertiesRestoreRule;
34  import org.apache.lucene.util.LuceneTestCase;
35  import org.apache.lucene.util.LuceneTestCase.SuppressSysoutChecks;
36  import org.apache.solr.SolrTestCaseJ4;
37  import org.apache.solr.client.solrj.SolrQuery;
38  import org.apache.solr.client.solrj.embedded.JettyConfig;
39  import org.apache.solr.client.solrj.embedded.JettyConfig.Builder;
40  import org.apache.solr.client.solrj.embedded.JettySolrRunner;
41  import org.apache.solr.client.solrj.impl.CloudSolrClient;
42  import org.apache.solr.client.solrj.response.QueryResponse;
43  import org.apache.solr.common.SolrInputDocument;
44  import org.apache.solr.common.cloud.ClusterState;
45  import org.apache.solr.common.cloud.Replica;
46  import org.apache.solr.common.cloud.Slice;
47  import org.apache.solr.common.cloud.SolrZkClient;
48  import org.apache.solr.common.cloud.ZkStateReader;
49  import org.apache.solr.core.CoreDescriptor;
50  import org.apache.solr.util.RevertDefaultThreadHandlerRule;
51  import org.junit.ClassRule;
52  import org.junit.Rule;
53  import org.junit.Test;
54  import org.junit.rules.RuleChain;
55  import org.junit.rules.TestRule;
56  import org.slf4j.Logger;
57  import org.slf4j.LoggerFactory;
58  
59  /**
60   * Test of the MiniSolrCloudCluster functionality. Keep in mind, 
61   * MiniSolrCloudCluster is designed to be used outside of the Lucene test
62   * hierarchy.
63   */
64  @SuppressSysoutChecks(bugUrl = "Solr logs to JUL")
65  public class TestMiniSolrCloudCluster extends LuceneTestCase {
66  
67    private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
68    protected int NUM_SERVERS = 5;
69    protected int NUM_SHARDS = 2;
70    protected int REPLICATION_FACTOR = 2;
71  
72    public TestMiniSolrCloudCluster () {
73      NUM_SERVERS = 5;
74      NUM_SHARDS = 2;
75      REPLICATION_FACTOR = 2;
76    }
77    
78    @Rule
79    public TestRule solrTestRules = RuleChain
80        .outerRule(new SystemPropertiesRestoreRule());
81    
82    @ClassRule
83    public static TestRule solrClassRules = RuleChain.outerRule(
84        new SystemPropertiesRestoreRule()).around(
85        new RevertDefaultThreadHandlerRule());
86    
87    private MiniSolrCloudCluster createMiniSolrCloudCluster() throws Exception {
88      Builder jettyConfig = JettyConfig.builder();
89      jettyConfig.waitForLoadingCoresToFinish(null);
90      return new MiniSolrCloudCluster(NUM_SERVERS, createTempDir(), jettyConfig.build());
91    }
92      
93    private void createCollection(MiniSolrCloudCluster miniCluster, String collectionName, String createNodeSet, String asyncId, boolean persistIndex) throws Exception {
94      String configName = "solrCloudCollectionConfig";
95      File configDir = new File(SolrTestCaseJ4.TEST_HOME() + File.separator + "collection1" + File.separator + "conf");
96      miniCluster.uploadConfigDir(configDir, configName);
97  
98      Map<String, String> collectionProperties = new HashMap<>();
99      collectionProperties.put(CoreDescriptor.CORE_CONFIG, "solrconfig-tlog.xml");
100     collectionProperties.put("solr.tests.maxBufferedDocs", "100000");
101     collectionProperties.put("solr.tests.ramBufferSizeMB", "100");
102     // use non-test classes so RandomizedRunner isn't necessary
103     collectionProperties.put("solr.tests.mergePolicy", "org.apache.lucene.index.TieredMergePolicy");
104     collectionProperties.put("solr.tests.mergeScheduler", "org.apache.lucene.index.ConcurrentMergeScheduler");
105     collectionProperties.put("solr.directoryFactory", (persistIndex ? "solr.StandardDirectoryFactory" : "solr.RAMDirectoryFactory"));
106     
107     miniCluster.createCollection(collectionName, NUM_SHARDS, REPLICATION_FACTOR, configName, createNodeSet, asyncId, collectionProperties);
108   }
109 
110   @Test
111   public void testCollectionCreateSearchDelete() throws Exception {
112 
113     final String collectionName = "testcollection";
114     MiniSolrCloudCluster miniCluster = createMiniSolrCloudCluster();
115 
116     final CloudSolrClient cloudSolrClient = miniCluster.getSolrClient();
117 
118     try {
119       assertNotNull(miniCluster.getZkServer());
120       List<JettySolrRunner> jettys = miniCluster.getJettySolrRunners();
121       assertEquals(NUM_SERVERS, jettys.size());
122       for (JettySolrRunner jetty : jettys) {
123         assertTrue(jetty.isRunning());
124       }
125 
126       // shut down a server
127       log.info("#### Stopping a server");
128       JettySolrRunner stoppedServer = miniCluster.stopJettySolrRunner(0);
129       assertTrue(stoppedServer.isStopped());
130       assertEquals(NUM_SERVERS - 1, miniCluster.getJettySolrRunners().size());
131 
132       // create a server
133       log.info("#### Starting a server");
134       JettySolrRunner startedServer = miniCluster.startJettySolrRunner();
135       assertTrue(startedServer.isRunning());
136       assertEquals(NUM_SERVERS, miniCluster.getJettySolrRunners().size());
137 
138       // create collection
139       log.info("#### Creating a collection");
140       final String asyncId = (random().nextBoolean() ? null : "asyncId("+collectionName+".create)="+random().nextInt());
141       createCollection(miniCluster, collectionName, null, asyncId, random().nextBoolean());
142       if (asyncId != null) {
143         assertEquals("did not see async createCollection completion", "completed", AbstractFullDistribZkTestBase.getRequestStateAfterCompletion(asyncId, 330, cloudSolrClient));
144       }
145 
146       ZkStateReader zkStateReader = miniCluster.getSolrClient().getZkStateReader();
147       AbstractDistribZkTestBase.waitForRecoveriesToFinish(collectionName, zkStateReader, true, true, 330);
148 
149       // modify/query collection
150       log.info("#### updating a querying collection");
151       cloudSolrClient.setDefaultCollection(collectionName);
152       SolrInputDocument doc = new SolrInputDocument();
153       doc.setField("id", "1");
154       cloudSolrClient.add(doc);
155       cloudSolrClient.commit();
156       SolrQuery query = new SolrQuery();
157       query.setQuery("*:*");
158       QueryResponse rsp = cloudSolrClient.query(query);
159       assertEquals(1, rsp.getResults().getNumFound());
160 
161       // remove a server not hosting any replicas
162       zkStateReader.updateClusterState();
163       ClusterState clusterState = zkStateReader.getClusterState();
164       HashMap<String, JettySolrRunner> jettyMap = new HashMap<String, JettySolrRunner>();
165       for (JettySolrRunner jetty : miniCluster.getJettySolrRunners()) {
166         String key = jetty.getBaseUrl().toString().substring((jetty.getBaseUrl().getProtocol() + "://").length());
167         jettyMap.put(key, jetty);
168       }
169       Collection<Slice> slices = clusterState.getSlices(collectionName);
170       // track the servers not host repliacs
171       for (Slice slice : slices) {
172         jettyMap.remove(slice.getLeader().getNodeName().replace("_solr", "/solr"));
173         for (Replica replica : slice.getReplicas()) {
174           jettyMap.remove(replica.getNodeName().replace("_solr", "/solr"));
175         }
176       }
177       assertTrue("Expected to find a node without a replica", jettyMap.size() > 0);
178       log.info("#### Stopping a server");
179       JettySolrRunner jettyToStop = jettyMap.entrySet().iterator().next().getValue();
180       jettys = miniCluster.getJettySolrRunners();
181       for (int i = 0; i < jettys.size(); ++i) {
182         if (jettys.get(i).equals(jettyToStop)) {
183           miniCluster.stopJettySolrRunner(i);
184           assertEquals(NUM_SERVERS - 1, miniCluster.getJettySolrRunners().size());
185         }
186       }
187 
188       // re-create a server (to restore original NUM_SERVERS count)
189       log.info("#### Starting a server");
190       startedServer = miniCluster.startJettySolrRunner(jettyToStop);
191       assertTrue(startedServer.isRunning());
192       assertEquals(NUM_SERVERS, miniCluster.getJettySolrRunners().size());
193 
194 
195       // delete the collection we created earlier
196       miniCluster.deleteCollection(collectionName);
197       AbstractDistribZkTestBase.waitForCollectionToDisappear(collectionName, zkStateReader, true, true, 330);
198 
199       // create it again
200       String asyncId2 = (random().nextBoolean() ? null : "asyncId("+collectionName+".create)="+random().nextInt());
201       createCollection(miniCluster, collectionName, null, asyncId2, random().nextBoolean());
202       if (asyncId2 != null) {
203         assertEquals("did not see async createCollection completion", "completed", AbstractFullDistribZkTestBase.getRequestStateAfterCompletion(asyncId2, 330, cloudSolrClient));
204       }
205       AbstractDistribZkTestBase.waitForRecoveriesToFinish(collectionName, zkStateReader, true, true, 330);
206 
207       // check that there's no left-over state
208       assertEquals(0, cloudSolrClient.query(new SolrQuery("*:*")).getResults().getNumFound());
209       cloudSolrClient.add(doc);
210       cloudSolrClient.commit();
211       assertEquals(1, cloudSolrClient.query(new SolrQuery("*:*")).getResults().getNumFound());
212 
213     }
214     finally {
215       miniCluster.shutdown();
216     }
217   }
218 
219   @Test
220   public void testErrorsInStartup() throws Exception {
221 
222     final AtomicInteger jettyIndex = new AtomicInteger();
223 
224     MiniSolrCloudCluster cluster = null;
225     try {
226       cluster = new MiniSolrCloudCluster(3, createTempDir(), JettyConfig.builder().build()) {
227         @Override
228         public JettySolrRunner startJettySolrRunner(String name, String context, JettyConfig config) throws Exception {
229           if (jettyIndex.incrementAndGet() != 2)
230             return super.startJettySolrRunner(name, context, config);
231           throw new IOException("Fake exception on startup!");
232         }
233       };
234       fail("Expected an exception to be thrown from MiniSolrCloudCluster");
235     }
236     catch (Exception e) {
237       assertEquals("Error starting up MiniSolrCloudCluster", e.getMessage());
238       assertEquals("Expected one suppressed exception", 1, e.getSuppressed().length);
239       assertEquals("Fake exception on startup!", e.getSuppressed()[0].getMessage());
240     }
241     finally {
242       if (cluster != null)
243         cluster.shutdown();
244     }
245   }
246 
247   @Test
248   public void testErrorsInShutdown() throws Exception {
249 
250     final AtomicInteger jettyIndex = new AtomicInteger();
251 
252     MiniSolrCloudCluster cluster = new MiniSolrCloudCluster(3, createTempDir(), JettyConfig.builder().build()) {
253         @Override
254         protected JettySolrRunner stopJettySolrRunner(JettySolrRunner jetty) throws Exception {
255           JettySolrRunner j = super.stopJettySolrRunner(jetty);
256           if (jettyIndex.incrementAndGet() == 2)
257             throw new IOException("Fake IOException on shutdown!");
258           return j;
259         }
260       };
261 
262     try {
263       cluster.shutdown();
264       fail("Expected an exception to be thrown on MiniSolrCloudCluster shutdown");
265     }
266     catch (Exception e) {
267       assertEquals("Error shutting down MiniSolrCloudCluster", e.getMessage());
268       assertEquals("Expected one suppressed exception", 1, e.getSuppressed().length);
269       assertEquals("Fake IOException on shutdown!", e.getSuppressed()[0].getMessage());
270     }
271 
272   }
273 
274   @Test
275   public void testExtraFilters() throws Exception {
276     Builder jettyConfig = JettyConfig.builder();
277     jettyConfig.waitForLoadingCoresToFinish(null);
278     jettyConfig.withFilter(JettySolrRunner.DebugFilter.class, "*");
279     MiniSolrCloudCluster cluster = new MiniSolrCloudCluster(NUM_SERVERS, createTempDir(), jettyConfig.build());
280     cluster.shutdown();
281   }
282 
283   @Test
284   public void testCollectionCreateWithoutCoresThenDelete() throws Exception {
285 
286     final String collectionName = "testSolrCloudCollectionWithoutCores";
287     final MiniSolrCloudCluster miniCluster = createMiniSolrCloudCluster();
288     final CloudSolrClient cloudSolrClient = miniCluster.getSolrClient();
289 
290     try {
291       assertNotNull(miniCluster.getZkServer());
292       assertFalse(miniCluster.getJettySolrRunners().isEmpty());
293 
294       // create collection
295       final String asyncId = (random().nextBoolean() ? null : "asyncId("+collectionName+".create)="+random().nextInt());
296       createCollection(miniCluster, collectionName, OverseerCollectionMessageHandler.CREATE_NODE_SET_EMPTY, asyncId, random().nextBoolean());
297       if (asyncId != null) {
298         assertEquals("did not see async createCollection completion", "completed", AbstractFullDistribZkTestBase.getRequestStateAfterCompletion(asyncId, 330, cloudSolrClient));
299       }
300 
301       try (SolrZkClient zkClient = new SolrZkClient
302           (miniCluster.getZkServer().getZkAddress(), AbstractZkTestCase.TIMEOUT, 45000, null);
303           ZkStateReader zkStateReader = new ZkStateReader(zkClient)) {
304         
305         // wait for collection to appear
306         AbstractDistribZkTestBase.waitForRecoveriesToFinish(collectionName, zkStateReader, true, true, 330);
307 
308         // check the collection's corelessness
309         {
310           int coreCount = 0; 
311           for (Map.Entry<String,Slice> entry : zkStateReader.getClusterState().getSlicesMap(collectionName).entrySet()) {
312             coreCount += entry.getValue().getReplicasMap().entrySet().size();
313           }
314           assertEquals(0, coreCount);
315         }
316         
317         // delete the collection we created earlier
318         miniCluster.deleteCollection(collectionName);
319         AbstractDistribZkTestBase.waitForCollectionToDisappear(collectionName, zkStateReader, true, true, 330);    
320       }
321     }
322     finally {
323       miniCluster.shutdown();
324     }
325   }
326 
327   @Test
328   public void testStopAllStartAll() throws Exception {
329 
330     final String collectionName = "testStopAllStartAllCollection";
331 
332     final MiniSolrCloudCluster miniCluster = createMiniSolrCloudCluster();
333 
334     try {
335       assertNotNull(miniCluster.getZkServer());
336       List<JettySolrRunner> jettys = miniCluster.getJettySolrRunners();
337       assertEquals(NUM_SERVERS, jettys.size());
338       for (JettySolrRunner jetty : jettys) {
339         assertTrue(jetty.isRunning());
340       }
341 
342       createCollection(miniCluster, collectionName, null, null, true);
343       final CloudSolrClient cloudSolrClient = miniCluster.getSolrClient();
344       cloudSolrClient.setDefaultCollection(collectionName);
345       final SolrQuery query = new SolrQuery("*:*");
346       final SolrInputDocument doc = new SolrInputDocument();
347 
348       try (SolrZkClient zkClient = new SolrZkClient
349           (miniCluster.getZkServer().getZkAddress(), AbstractZkTestCase.TIMEOUT, 45000, null);
350           ZkStateReader zkStateReader = new ZkStateReader(zkClient)) {
351         AbstractDistribZkTestBase.waitForRecoveriesToFinish(collectionName, zkStateReader, true, true, 330);
352 
353         // modify collection
354         final int numDocs = 1 + random().nextInt(10);
355         for (int ii = 1; ii <= numDocs; ++ii) {
356           doc.setField("id", ""+ii);
357           cloudSolrClient.add(doc);
358           if (ii*2 == numDocs) cloudSolrClient.commit();
359         }
360         cloudSolrClient.commit();
361         // query collection
362         {
363           final QueryResponse rsp = cloudSolrClient.query(query);
364           assertEquals(numDocs, rsp.getResults().getNumFound());
365         }
366 
367         // the test itself
368         zkStateReader.updateClusterState();
369         final ClusterState clusterState = zkStateReader.getClusterState();
370 
371         final HashSet<Integer> leaderIndices = new HashSet<Integer>();
372         final HashSet<Integer> followerIndices = new HashSet<Integer>();
373         {
374           final HashMap<String,Boolean> shardLeaderMap = new HashMap<String,Boolean>();
375           for (final Slice slice : clusterState.getSlices(collectionName)) {
376             for (final Replica replica : slice.getReplicas()) {
377               shardLeaderMap.put(replica.getNodeName().replace("_solr", "/solr"), Boolean.FALSE);
378             }
379             shardLeaderMap.put(slice.getLeader().getNodeName().replace("_solr", "/solr"), Boolean.TRUE);
380           }
381           for (int ii = 0; ii < jettys.size(); ++ii) {
382             final URL jettyBaseUrl = jettys.get(ii).getBaseUrl();
383             final String jettyBaseUrlString = jettyBaseUrl.toString().substring((jettyBaseUrl.getProtocol() + "://").length());
384             final Boolean isLeader = shardLeaderMap.get(jettyBaseUrlString);
385             if (Boolean.TRUE.equals(isLeader)) {
386               leaderIndices.add(new Integer(ii));
387             } else if (Boolean.FALSE.equals(isLeader)) {
388               followerIndices.add(new Integer(ii));
389             } // else neither leader nor follower i.e. node without a replica (for our collection)
390           }
391         }
392         final List<Integer> leaderIndicesList = new ArrayList<Integer>(leaderIndices);
393         final List<Integer> followerIndicesList = new ArrayList<Integer>(followerIndices);
394 
395         // first stop the followers (in no particular order)
396         Collections.shuffle(followerIndicesList, random());
397         for (Integer ii : followerIndicesList) {
398           if (!leaderIndices.contains(ii)) {
399             miniCluster.stopJettySolrRunner(jettys.get(ii.intValue()));
400           }
401         }
402 
403         // then stop the leaders (again in no particular order)
404         Collections.shuffle(leaderIndicesList, random());
405         for (Integer ii : leaderIndicesList) {
406           miniCluster.stopJettySolrRunner(jettys.get(ii.intValue()));
407         }
408 
409         // calculate restart order
410         final List<Integer> restartIndicesList = new ArrayList<Integer>();
411         Collections.shuffle(leaderIndicesList, random());
412         restartIndicesList.addAll(leaderIndicesList);
413         Collections.shuffle(followerIndicesList, random());
414         restartIndicesList.addAll(followerIndicesList);
415         if (random().nextBoolean()) Collections.shuffle(restartIndicesList, random());
416 
417         // and then restart jettys in that order
418         for (Integer ii : restartIndicesList) {
419           final JettySolrRunner jetty = jettys.get(ii.intValue());
420           if (!jetty.isRunning()) {
421             miniCluster.startJettySolrRunner(jetty);
422             assertTrue(jetty.isRunning());
423           }
424         }
425         AbstractDistribZkTestBase.waitForRecoveriesToFinish(collectionName, zkStateReader, true, true, 330);
426 
427         zkStateReader.updateClusterState();
428 
429         // re-query collection
430         {
431           final QueryResponse rsp = cloudSolrClient.query(query);
432           assertEquals(numDocs, rsp.getResults().getNumFound());
433         }
434 
435         // delete the collection we created earlier
436         miniCluster.deleteCollection(collectionName);
437         AbstractDistribZkTestBase.waitForCollectionToDisappear(collectionName, zkStateReader, true, true, 330);
438       }
439     }
440     finally {
441       miniCluster.shutdown();
442     }
443   }
444 
445 }